/*
 * MIT License
 *
 * Copyright (c) 2019-2021 JetBrains s.r.o.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package org.jetbrains.projector.client.web

import kotlinx.browser.window
import org.jetbrains.projector.client.common.SingleRenderingSurfaceProcessor.Companion.shrinkByPaintEvents
import org.jetbrains.projector.client.common.misc.ImageCacher
import org.jetbrains.projector.client.web.component.MarkdownPanelManager
import org.jetbrains.projector.client.web.input.InputController
import org.jetbrains.projector.client.web.misc.PingStatistics
import org.jetbrains.projector.client.web.speculative.Typing
import org.jetbrains.projector.client.web.state.ProjectorUI
import org.jetbrains.projector.client.web.window.OnScreenMessenger
import org.jetbrains.projector.client.web.window.WindowDataEventsProcessor
import org.jetbrains.projector.common.misc.Do
import org.jetbrains.projector.common.protocol.toClient.*
import org.jetbrains.projector.util.logging.Logger
import org.w3c.dom.url.URL

/**
 * ServerEvent 处理器【重要】
 * 处理各种 Server=》Client 的事件
 */
class ServerEventsProcessor(private val windowDataEventsProcessor: WindowDataEventsProcessor) {

  @OptIn(ExperimentalStdlibApi::class)
  fun process(commands: ToClientMessageType, pingStatistics: PingStatistics, typing: Typing, markdownPanelManager: MarkdownPanelManager,
              inputController: InputController) {
    // 绘图相关的事件的数组。主要是遍历 commands 后，再去处理绘制相关的动作
    val drawCommandsEvents = mutableListOf<ServerDrawCommandsEvent>()

    commands.forEach { command ->
      Do exhaustive when (command) {
        // 窗口发生变化的事件，@see projector-server 的 ProjectorServer 的 areChangedWindows 方法的逻辑
        is ServerWindowSetChangedEvent -> {
          windowDataEventsProcessor.process(command)
          markdownPanelManager.updatePlacements()
        }

        // 绘图事件，添加到 drawCommandsEvents 中
        is ServerDrawCommandsEvent -> drawCommandsEvents.add(command)

        // 获取图片数据的响应事件
        is ServerImageDataReplyEvent -> windowDataEventsProcessor.windowManager.imageCacher.putImageData(
          command.imageId,
          command.imageData,
        )

        // TODO 芋艿：idea 独有，后续在看
        is ServerCaretInfoChangedEvent -> {
          typing.changeCaretInfo(command.data)
          inputController.handleCaretInfoChange(command.data)
        }

        // 复制操作的响应事件，会将内容添加到浏览器的粘贴板里
        is ServerClipboardEvent -> handleServerClipboardChange(command)

        // TODO 芋艿：ping 的回包
        is ServerPingReplyEvent -> pingStatistics.onPingReply(command)

        // TODO 芋艿：markdown 相关，暂时忽略
        is ServerMarkdownEvent -> when (command) {
          is ServerMarkdownEvent.ServerMarkdownShowEvent -> markdownPanelManager.show(command.panelId, command.show)
          is ServerMarkdownEvent.ServerMarkdownResizeEvent -> markdownPanelManager.resize(command.panelId, command.size)
          is ServerMarkdownEvent.ServerMarkdownMoveEvent -> markdownPanelManager.move(command.panelId, command.point)
          is ServerMarkdownEvent.ServerMarkdownDisposeEvent -> markdownPanelManager.dispose(command.panelId)
          is ServerMarkdownEvent.ServerMarkdownPlaceToWindowEvent -> markdownPanelManager.placeToWindow(command.panelId, command.windowId)
          is ServerMarkdownEvent.ServerMarkdownSetHtmlEvent -> markdownPanelManager.setHtml(command.panelId, command.html)
          is ServerMarkdownEvent.ServerMarkdownSetCssEvent -> markdownPanelManager.setCss(command.panelId, command.css)
          is ServerMarkdownEvent.ServerMarkdownScrollEvent -> markdownPanelManager.scroll(command.panelId, command.scrollOffset)
          is ServerMarkdownEvent.ServerMarkdownBrowseUriEvent -> browseUri(command.link)
        }

        is ServerWindowColorsEvent -> {
          ProjectorUI.setColors(command.colors)
          // todo: should WindowManager.lookAndFeelChanged() be called here?
          OnScreenMessenger.lookAndFeelChanged()
        }
      }
    }

    // todo: determine the moment better
    if (drawCommandsEvents.any { it.drawEvents.any { drawEvent -> drawEvent is ServerDrawStringEvent } }) {
      typing.removeSpeculativeImage()
    }

    // TODO 芋艿：后面在理解这个排序的意图
    drawCommandsEvents.sortWith(drawingOrderComparator)

    drawCommandsEvents.forEach { event ->
      Do exhaustive when (val target = event.target) {
        is ServerDrawCommandsEvent.Target.Onscreen -> windowDataEventsProcessor.draw(target.windowId, event.drawEvents)

        is ServerDrawCommandsEvent.Target.Offscreen -> {
          // 获得图片对应的 SingleRenderingSurfaceProcessor 对象
          val offscreenProcessor = windowDataEventsProcessor.windowManager.imageCacher.getOffscreenProcessor(target)
          // 执行绘制
          val drawEvents = event.drawEvents.shrinkByPaintEvents()
          val firstUnsuccessful = offscreenProcessor.processNew(drawEvents)
          if (firstUnsuccessful != null) {
            // todo: remember unsuccessful events and redraw pending ones as for windows
            logger.error { "Encountered unsuccessful drawing for an offscreen surface ${target.pVolatileImageId}, skipping" }
          }
          // TODO 芋艿：代码优化，感觉凑一个类似 windowDataEventsProcessor 的实现，会更统一一点。

          // 绘制 pending 的事件。解决图片不存在的时候，draw 失败的情况。
          // @see SingleRenderingSurfaceProcessor 的 handleDrawEvent 方法，涉及到图片不存在的情况。
          windowDataEventsProcessor.drawPendingEvents()
        }
      }
    }
  }

  fun onResized() {
    windowDataEventsProcessor.onResized()
  }

  /**
   * 将内容添加到浏览器的粘贴板里
   */
  private fun handleServerClipboardChange(event: ServerClipboardEvent) {
    window.navigator.clipboard.writeText(event.stringContent)
      .catch { logger.error { "Error writing clipboard: $it" } }
  }

  private fun browseUri(link: String) {
    val url = URL(link)

    if(url.hostname == "localhost" || url.hostname == "127.0.0.1" || url.host == "::1") {
      url.hostname = window.location.hostname
      url.protocol = window.location.protocol
    }

    val popUpWindow = window.open(url.href, "_blank")

    if (popUpWindow != null) {
      popUpWindow.focus()  // browser has allowed it to be opened
    }
    else {
      window.alert("To open $link, please allow popups for this website")  // browser has blocked it
    }
  }

  companion object {

    private val logger = Logger<ServerEventsProcessor>()

    // todo: sorting is added only as a hacky temporary workaround for PRJ-20.
    //       Please see commit description for details how this should be fixed
    private val drawingOrderComparator = compareBy<ServerDrawCommandsEvent>(
      // render offscreen surfaces first
      {
        when (it.target) {
          is ServerDrawCommandsEvent.Target.Offscreen -> 0
          is ServerDrawCommandsEvent.Target.Onscreen -> 1
        }
      },
      // render older surfaces last
      {
        when (val target = it.target) {
          is ServerDrawCommandsEvent.Target.Offscreen -> -target.pVolatileImageId
          is ServerDrawCommandsEvent.Target.Onscreen -> -target.windowId
        }
      },
    )
  }
}
